/**
 * TODO: Vložit licenci.
 */

package cz.enif.commander;

import java.util.ArrayList;
import java.util.List;

import cz.enif.commander.values.IValue;
import cz.enif.commander.values.ValueFactory;

/**
 * Analyzuje vstupní proud znaků a skládá z něj tokeny.
 *
 * @author farap
 */
final class Parser implements IError
{
    //// CONSTANTS ////

	/**
	 * Typ tokenu.
	 */
    enum TokenType
    {
    	/** Neznámý token. */
        UNKNOWN,

        /** Identifikátor. */
        IDENTIFIER,             // ideNtifikator23
        
        /** Celé číslo. */
        INT,                    // 145
        
        /** Reálné číslo. */
        DOUBLE,                 // 1.5e3
        
        /** Řetězec. */
        STRING,                 // "řetězec"

        /** Oddělovač položek v tuple. */
        TUPLE_SEPARATOR,        // '/'
        
        /** Oddělovač příkazů. */
        COMMAND_SEPARATOR,      // \n nebo ;
        
        /** Začátek komentáře. */
        COMMENT_START,          // #

        /** Klíčové slovo true. */
        TRUE,					// true
        
        /** Klíčové slovo false. */
        FALSE,					// false
        
        /** Konec vstupu (end-of-file). */
        EOF                     // konec vstupu/skriptu
    }

    //// CONSTRUCTORS ////

    /**
     * Konstruktor.
     * 
     * @param scanner Inicializovaný scanner, který bude použit pro čtení.
     */
    public Parser(Scanner scanner)
    {
        // ještě nenastala žádná chyba...
        this.clearError();
    	
        // nastavíme scanner
    	this.scanner = scanner;
        if (this.scanner == null)
        {
            this.setError("Nezadán scanner.");
            
            return;
        }
        
        // načteme první token, případná chyba se zaznamená a použije později
        this.nextToken();
    }

    //// PUBLIC ////

    /**
     * Načte a zkompiluje vstup (zdrojový skript) do vnitřní reprezentace.
     * 
     * @return Seznam obsahující vnitřní reprezentaci zkompilovaného skriptu.
     *   Dojde-li k chybě při překladu, vrací null. 
     */
    public List<Command> parse()
    {
        // při chybě okamžitě konec a vracíme null
        if (this.wasError()) return null;
        
        ArrayList<Command> commands = new ArrayList<Command>();

        while (this.tokenType != TokenType.EOF)
        {
        	// načteme další command
        	Command command = this.parseCommand();
        	
        	// při chybě okamžitě konec a vracíme null
            if (this.wasError()) return null;
            
            // už žádný příkaz není - jsme na konci
            if (command == null) break;
            
            // přidáme příkaz do seznamu příkazů.
            commands.add(command);
        }

        return commands;
    }
    
    /**
     * Parsuje jeden příkaz.
     *
     * <pre>
     * command :: command-identifier { argument } .
     * 
     * command-identifier :: identifier | string-literal .
     * </pre>
     * 
     * @return Objekt typu ICommand reprezentující načtený příkaz, 
     *   při chybě, nebo na konci vstupu null.
     */
    public Command parseCommand()
    {
    	// pokud došlo k chybě, skončíme
        if (this.wasError()) return null;
    	        
        // přeskakujeme komentáře a prázdné příkazy
        if (this.skipNonCommands() == false) return null;

        // skončíme, pokud už nejsou žádné další tokeny
        if (this.tokenType == TokenType.EOF)
        {
            return null;
        }

        // očekáváme identifikátor příkazu
        if (this.tokenType != TokenType.IDENTIFIER && this.tokenType != TokenType.STRING)
        {
        	this.setError("Očekáván identifikátor příkazu.");

            return null;
        }

        // zapamatujeme si jméno příkazu
        String commandName = this.sValue;

        // přejdeme na token za jménem příkazu
        if (this.nextToken() == false) return null;

        // získáme parametry příkazu
        List<IValue> params = new ArrayList<IValue>();
        while (this.tokenType != TokenType.EOF)
        {
            if (this.tokenType == TokenType.COMMAND_SEPARATOR)
            {
                break;
            }

            if (parseArgument(params) == false)
            {
                return null;
            }
        }

        // pozřeme oddělovač příkazů
        if (this.nextToken() == false) return null;
        
        // vrátíme příkaz
        return new Command(commandName, params);
    }

    /* (non-Javadoc)
	 * @see cz.enif.commander.IError#wasError()
	 */
	@Override
	public boolean wasError() 
	{
		return this.wasError;
	}

	/* (non-Javadoc)
	 * @see cz.enif.commander.IError#getErrorMessage()
	 */
	@Override
	public String getErrorMessage() 
	{
		return this.errorMessage;
	}
    
    //// PRIVATE ////

    private Scanner scanner;

    private TokenType tokenType;
    private int iValue;
    private double dValue;
    private String sValue;

    private boolean wasError;
    private String errorMessage;
    
    /**
     * Vymaže popis chyby.
     */
    private void clearError()
    {
    	this.wasError = false;
    	this.errorMessage = "OK";
    }
    
    /**
     * Nastaví příznak chyby a její popis.
     * 
     * @param message Popis chyby.
     */
    private void setError(String message)
    {
    	this.wasError = true;
    	this.errorMessage = (message == null ? "ERROR" : message);
    	
    	// "Chyba." -> "[řádka] Chyba.
    	if (this.scanner != null)
    	{
    		this.errorMessage = "[" + this.scanner.getLine() + "] " + this.errorMessage;
    	}
    }
    
    /**
     * Parsuje argument.
     * 
     * <pre>
     * argument :: tuple .
     * </pre>
     * 
     * @param params Seznam parametrů překládaného příkazu.
     * 
     * @return True při úspěchu, jinak false.
     */
    private boolean parseArgument(List<IValue> params)
    {
        return this.parseTuple(params);
    }
    
    /**
     * Parsuje tuple.
     * 
     * <pre>
     * tuple :: value { '/' [ value ] } [ '/' value ] .
     * </pre> 
     * 
     * @param params Seznam parametrů překládaného příkazu.
     * 
     * @return True při úspěchu, jinak false.
     */
    private boolean parseTuple(List<IValue> params)
    {
    	// Seznam hodnot tuple.
        List<IValue> values = new ArrayList<IValue>();

        // Vložíme první hodnotu do tuple.
        if (this.addValue(values) == false) return false;

        // Pozřeme hodnotu.
        if (this.nextToken() == false) return false;

        // Pokud už nejsou další data pro tuple,
        // vložíme hodnotu přímo do argumentů příkazu.
        if (this.tokenType != TokenType.TUPLE_SEPARATOR)
        {
        	params.add(values.get(0));

            return true;
        }

        // { '/' value } [ '/' value ]
        while (this.tokenType == TokenType.TUPLE_SEPARATOR)
        {
            // pozřeme TUPLE_SEPARATOR
            if (this.nextToken() == false) return false;

            // vložíme hodnotu do tuple
            if (this.addValue(values) == false) return false;

            // Hodnota není povinná, takže mohou bezprostředně po sobě
            // následovat dva tokeny TUPLE_SEPARATOR. Pokud hodnota byla
            // zadána, pozřeme ji, pokud ne, nesmíme pozřít TUPLE_SEPARATOR!
            if (this.tokenType != TokenType.TUPLE_SEPARATOR)
            {
                // pozřeme hodnotu
                if (this.nextToken() == false) return false;
            }
        }

        // vložíme všechny hodnoty tuple do parametrů
        return params.add(ValueFactory.valueOf(values));
    }
    
    /**
     * Přidá do seznamu hodnot novou hodnotu podle typu načteného tokenu.
     * Pro neuvedené hodnoty ('//') vloží int 0.
     * 
     * @param values Seznam hodnot, do kterého se má načtená hodnota vložit.
     * 
     * @return True při úspěchu, jinak false.
     */
    private boolean addValue(List<IValue> values)
    {
        switch (this.tokenType)
        {
            case INT:
                values.add(ValueFactory.valueOf(this.iValue));
                break;

            case DOUBLE:
                values.add(ValueFactory.valueOf(this.dValue));
                break;

            case IDENTIFIER:
            case STRING:
                values.add(ValueFactory.valueOf(this.sValue));
                break;

              case TRUE:
            	values.add(ValueFactory.valueOf(true));
        		break;
        		
        	case FALSE:
        		values.add(ValueFactory.valueOf(false));
        		break;                
                
            case TUPLE_SEPARATOR:
                values.add(ValueFactory.valueOf(0));
                break;

            default:
            case UNKNOWN:
            	this.setError("Neznámý, nebo neočekávaný token.");
                return false;
        }

        return true;
    }

    /**
     * Přeskočí všechny prázdné řádky a komentáře.
     * 
     * Vrací true při úspěchu, jinak false.
     */
    private boolean skipNonCommands() 
	{
		while (this.tokenType == TokenType.COMMENT_START || this.tokenType == TokenType.COMMAND_SEPARATOR)
        {
            if (this.tokenType == TokenType.COMMENT_START)
            {
                if (this.skipComment() == false) return false;
            }

            if (this.tokenType == TokenType.COMMAND_SEPARATOR)
            {
                if (this.nextToken() == false) return false;
            }
        }
		
		return true;
	}
    
    /**
     * Přeskočí komentář začínající znakem '#' a končící koncem řádku.
     * Po přeskočení komentáře je načten znak za koncem komentáře.
     * 
     * @return True při úspěchu, jinak false.
     */
    private boolean skipComment()
    {
        this.scanner.nextChar();
        while (this.scanner.isNotEOF())
        {
            if (this.scanner.isEOLN()) break;

            this.scanner.nextChar();
        }

        // pozřeme EOLN
        this.scanner.nextChar();

        // načteme token za komentářem
        return this.nextToken();
    }

    /**
     * Načte identifikátor ze vstupu.
     *
     * <pre>
     * identifier :: non-digit { non-dogit | digit | '.' } .
     * 
     * digit :: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' .
	 *
	 * non-digit :: '_' | 'a' .. 'z' | 'A' .. 'Z' .
     * </pre>
     * 
     * @return True při úspěchu, jinak false.
     */
    private boolean parseIdent()
    {
        StringBuilder sb = new StringBuilder();

        while (this.scanner.isNonDigit() || this.scanner.isDigit() || this.scanner.is('.'))
        {
            sb.append((char)this.scanner.getLook());
            this.scanner.nextChar();
        }

        this.sValue = sb.toString();
        this.tokenType = TokenType.IDENTIFIER;

        return true;
    }

    /**
     * Načte celočíselnou nebo reálnou konstantu ze vstupu.
     *
     * A numeric constant is defined as:
     *
     * <pre>
     * number :: [ sign ] digit-sequence [ fractional-part ] [ scale-factor ] .
     * fractional-part :: '.' digit-sequence .
     * scale-factor :: ( 'e' | 'E' ) [ scale-factor-sign ]  digit-sequence .
     * digit-sequence :: digit { digit } .
     * scale-factor-sign :: sign
     * sign :: '+' | '-' .
     * digit :: '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' .
     * </pre>
     *
     * @return True při úspěchu, jinak false.
     */
    private boolean parseNumber()
    {
        boolean isDouble = false;
        int i = 0;
        int sign = 1;
        double d = 0.0;

        // přeskočíme bílé znaky
        while (this.scanner.isWhite())
        {
            this.scanner.nextChar();
        }

        // [ sign ]
        sign = 1;
        if (this.scanner.is('-'))
        {
            sign = -1;                 // set sign
            this.scanner.nextChar();             // eat '-'
        }
        else if (this.scanner.is('+'))
        {
            this.scanner.nextChar();             // eat '+'
        }

        // digit-sequence expected
        if (this.scanner.isDigit() == false)
        {
        	this.setError("Očekávána posloupnost číslic.");
            this.tokenType = TokenType.UNKNOWN;

            return false;
        }

        // integer-part
        i = 0;
        while (this.scanner.isDigit())
        {
            i *= 10;
            i += (int)(this.scanner.getLook() - '0');
            this.scanner.nextChar();
        }

        // [ fractional-part ]
        if (this.scanner.is('.'))
        {
            isDouble = true;
            d = (double)i;

            this.scanner.nextChar();      // eat '.'

            // digit-sequence expected
            if (this.scanner.isDigit() == false)
            {
            	this.setError("Očekávána posloupnost číslic po desetiné čárce.");
                this.tokenType = TokenType.UNKNOWN;

                return false;
            }

            // read the fractional-part
            double frac = 0.0;
            double frac_scale = 0.1;
            while (this.scanner.isDigit())
            {
                frac += frac_scale * ((double)(this.scanner.getLook() - '0'));
                frac_scale *= 0.1;
                this.scanner.nextChar();
            }

            // add fractional-part to the real part
            d += frac;
        }

        // [ scale-factor ]
        if (this.scanner.is('e') || this.scanner.is('E'))
        {
            isDouble = true;
            d = (double)i;

            this.scanner.nextChar();      // eat the 'e' or the 'E'

            // [ scale-factor-sign ]
            double sfsign = 1.0;
            if (this.scanner.is('-'))
            {
                sfsign = -1.0;                 // set sign
                this.scanner.nextChar();               // eat '-'
            }
            else if (this.scanner.is('+'))
            {
                this.scanner.nextChar();               // eat '+'
            }

            // digit-sequence expected
            if (this.scanner.isDigit())
            {
            	this.setError("Očekávána posloupnost číslic - mocnitel.");
                this.tokenType = TokenType.UNKNOWN;

                return false;
            }

            // read the scale-factor
            double sf = 0.0;
            while (this.scanner.isDigit())
            {
                sf *= 10.0;
                sf += (double)(this.scanner.getLook() - '0');
                this.scanner.nextChar();
            }

            // aplly a scale-factor-sign to the scale-factor
            sf *= sfsign;

            // apply the scale-factor to the n
            d *= Math.pow(10.0, sf);
        }

        if (isDouble)
        {
            // aply the sign to the number
            d *= (double)sign;

            // return the parsed number
            this.dValue = d;
            this.tokenType = TokenType.DOUBLE;
        }
        else
        {
            // aply the sign to the number
            i *= sign;

            // return the parsed number
            this.iValue = i;
            this.tokenType = TokenType.INT;
        }

        return true;
    }


    /**
     * Načte řetězcový literál ze vstupu a vrátí jej jako token.
     *
     * <pre>
     * string-literal :: string-terminator { char } string-terminator .
     * string-terminator :: '"' || ''' .
     * </pre>
     *
     * Pokud je před koncem řádky znak '_' je tento ignorován a literál
     * pokračuje na další řádce.
     *
     * @return True, pokud bylo čtení v pořádku, jinak false.
     */
    private boolean parseString()
    {
        // začínáme na " nebo '
        int terminator = this.scanner.getLook();

        // přejdeme na znak po terminátoru (" nebo ')
        this.scanner.nextChar();

        StringBuilder sb = new StringBuilder();

        while (this.scanner.isNotEOF())
        {
        	// konec řádku
        	if (this.scanner.isEOLN()) break;
        	
        	// konec řetězce
            if (this.scanner.is(terminator)) break;

            // přeskočení konce řádku
            if (this.scanner.is('_'))
            {
            	// očekáváme konec řádku
            	this.scanner.nextChar();
            	
            	// je to '\r\n'?
            	if (this.scanner.is('\r'))
            	{
            		// pozřeme '\r' 
            		this.scanner.nextChar();
            		
            		// očekáváme '\n'
            		if (this.scanner.isEOLN() == false)
            		{
            			// vložíme pozřený '_'
                		sb.append('_');
                		
            			// vložíme pozřený '\r'
                		sb.append('\r');
                		
                		// zpracujeme znak za '\r'
                		continue;
            		}
            	}
            		
            	// očekáváme '\n'
            	if (this.scanner.isEOLN())
            	{
            		// pozřeme konec řádku 
            		this.scanner.nextChar();
            	}
            	else
            	{
            		// vložíme pozřený '_'
            		sb.append('_');
            	}
            	
            	// zpracujeme znak za '_' nebo koncem řádky
            	continue;
            }
            
            sb.append((char)this.scanner.getLook());
            this.scanner.nextChar();
        }

        // pokud byl nalezen regulérní konec řetězce
        if (this.scanner.is(terminator))
        {
            // pohltíme ho
            this.scanner.nextChar();

            // vrátíme nalezený řetězec
            this.sValue = sb.toString();
            this.tokenType = TokenType.STRING;
        }
        else
        {
        	// neuzavřený řetězec - chyba
        	this.setError("Neukončený řetězcový literál.");
            this.tokenType = TokenType.UNKNOWN;

            return false;
        }

        return true;
    }

    /**
     * Načte ze vstupu další token.
     *
     *  @return True, pokud se načtení tokenu zdařilo, jinak false.
     */
    private boolean nextToken()
    {
    	// při chybě již nepokračujeme
    	if (this.wasError()) return false;
    	
        // přeskočíme bílé znaky
        while (this.scanner.isWhite())
        {
            this.scanner.nextChar();
        }

        if (this.scanner.isNonDigit())
        {
            // identifikátor
            if (this.parseIdent() == false) return false;
            
            // klíčová slova
            if (this.sValue.contentEquals("true"))
            {
            	this.tokenType = TokenType.TRUE;
            }
            else if (this.sValue.contentEquals("false"))
            {
            	this.tokenType = TokenType.FALSE;
            }

            return true;
        }
        else if (this.scanner.isDigit() || this.scanner.is('+') || this.scanner.is('-'))
        {
            // číslo
            return this.parseNumber();
        }
        else if (this.scanner.is('"') || this.scanner.is('\''))
        {
            // řetězec
            return this.parseString();
        }
        else
        {
            switch (this.scanner.getLook())
            {
                case -1:
                    this.tokenType = TokenType.EOF;
                    break;

                case '#':
                    this.tokenType = TokenType.COMMENT_START;
                    break;

                case '\n':
                case ';':
                    this.tokenType = TokenType.COMMAND_SEPARATOR;
                    this.scanner.nextChar();
                    break;
                   
                case '/':
                    this.tokenType = TokenType.TUPLE_SEPARATOR;
                    this.scanner.nextChar();
                    break;

                default:
                	this.setError("Neznámý token.");
                    this.tokenType = TokenType.UNKNOWN;
                    return false;
            }
        }

        return true;
    }
}
